package com.nebula.core.dsl;

import org.neo4j.cypherdsl.core.Properties;
import org.neo4j.cypherdsl.core.*;
import org.neo4j.cypherdsl.core.ast.ProvidesAffixes;
import org.neo4j.cypherdsl.core.ast.TypedSubtree;
import org.neo4j.cypherdsl.core.ast.Visitable;
import org.neo4j.cypherdsl.core.internal.*;
import org.neo4j.cypherdsl.core.renderer.Renderer;
import org.neo4j.cypherdsl.core.utils.LRUCache;
import org.neo4j.cypherdsl.core.utils.Strings;

import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * nebula的cypher的渲染器
 *
 * @author zhangtao
 */
public class NebulaCypherRenderer implements Renderer {
    private static NebulaCypherRenderer nebulaCypherRenderer = new NebulaCypherRenderer();
    private final int STATEMENT_CACHE_SIZE = 128;
    private final LinkedHashMap<Integer, String> renderedStatementCache = new LRUCache<>(STATEMENT_CACHE_SIZE);
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock read = lock.readLock();
    private final Lock write = lock.writeLock();

    private NebulaCypherRenderer() {
    }

    public static NebulaCypherRenderer create() {
        return nebulaCypherRenderer;
    }

    @Override
    public String render(Statement statement) {
        int key = Objects.hash(statement, statement.isRenderConstantsAsParameters());
        String renderedContent;
        read.lock();
        try {
            renderedContent = renderedStatementCache.get(key);
        } finally {
            read.unlock();
        }

        if (renderedContent == null) {
            write.lock();
            try {
                NebulaVisitor renderingVisitor = new NebulaVisitor(statement.getContext());
                statement.accept(renderingVisitor);
                renderedContent = renderingVisitor.getRenderedContent().trim();
                renderedStatementCache.put(key, renderedContent);
            } finally {
                write.unlock();
            }
        }
        return renderedContent;
    }

}

class NebulaVisitor extends ReflectiveVisitor {

    private static final Pattern LABEL_AND_TYPE_QUOTATION = Pattern.compile("`");

    private static final EnumSet<Operator> SKIP_SPACES = EnumSet.of(Operator.EXPONENTIATION, Operator.UNARY_MINUS, Operator.UNARY_PLUS);

    /**
     * Target of all rendering.
     */
    protected final StringBuilder builder = new StringBuilder();

    /**
     * This keeps track on which level of the tree a separator is needed.
     */
    private final Map<Integer, AtomicReference<String>> separatorOnLevel = new ConcurrentHashMap<>();

    /**
     * Keeps track of named objects that have been already visited.
     */
    private final Deque<Set<Named>> dequeOfVisitedNamed = new ArrayDeque<>(new HashSet<>());

    /**
     * A set of aliased expressions that already have been seen and for which an alias must be used on each following
     * appearance.
     */
    protected final Set<AliasedExpression> visitableToAliased = new HashSet<>();

    /**
     * Keeps track if currently in an aliased expression so that the content can be skipped when already visited.
     */
    private final Deque<AliasedExpression> currentAliasedElements = new ArrayDeque<>();

    /**
     * A flag if we can skip aliasing. This is currently the case in exactly one scenario: A aliased expression passed
     * to a map project. In that case, the alias is already defined by the key to use in the projected map and we
     * cannot define him in `AS xxx` fragment.
     */
    private boolean skipAliasing = false;

    /**
     * Keeps track of unresolved symbolic names.
     */
    private final Map<SymbolicName, String> resolvedSymbolicNames = new ConcurrentHashMap<>();

    protected final StatementContext statementContext;

    /**
     * The current level in the tree of cypher elements.
     */
    private int currentLevel = 0;

    /**
     * Will be set to true when entering an already visited node.
     */
    private boolean skipNodeContent = false;

    /**
     * Will be set to true when entering an already visited relationship.
     */
    private boolean skipRelationshipContent = false;

    /**
     * Will be true when inside a {@link RelationshipPatternCondition}.
     */
    private boolean inRelationshipCondition = false;

    /**
     * Will be set to true when entering a {@link RelationshipPatternCondition}: In this case only existing symbolic names
     * may be used, new ones must not be introduced. We can deduce from looking at {@link #dequeOfVisitedNamed} which will
     * contain the list of named elements in the scope of the current subquery or with clause.
     */
    private boolean skipSymbolicName = false;

    NebulaVisitor(StatementContext statementContext) {
        this.statementContext = statementContext;
        this.dequeOfVisitedNamed.push(new HashSet<>());
    }

    private void enableSeparator(int level, boolean on) {
        if (on) {
            separatorOnLevel.put(level, new AtomicReference<>(""));
        } else {
            separatorOnLevel.remove(level);
        }
    }

    private Optional<AtomicReference<String>> separatorOnCurrentLevel() {
        return Optional.ofNullable(separatorOnLevel.get(currentLevel));
    }

    private String resolve(SymbolicName symbolicName) {
        return this.resolvedSymbolicNames.computeIfAbsent(symbolicName, k -> {
            String value = k.getValue();
            if (Strings.hasText(value)) {
                return escapeIfNecessary(symbolicName.getValue());
            }
            return String.format("%s%03d", Strings.randomIdentifier(8), resolvedSymbolicNames.size());
        });
    }

    @Override
    protected boolean preEnter(Visitable visitable) {
        Visitable lastAliased = currentAliasedElements.peek();
        if (skipNodeContent || visitableToAliased.contains(lastAliased)) {
            return false;
        }

        if (visitable instanceof ProvidesAffixes) {
            ((ProvidesAffixes) visitable).getPrefix().ifPresent(this::doWithPrefix);
        }

        if (visitable instanceof AliasedExpression) {
            currentAliasedElements.push((AliasedExpression) visitable);
        }

        if (visitable instanceof MapProjection) {
            this.skipAliasing = true;
        }

        int nextLevel = ++currentLevel + 1;
        if (visitable instanceof TypedSubtree) {
            enableSeparator(nextLevel, true);
        }

        separatorOnCurrentLevel().ifPresent(ref -> builder.append(ref.getAndSet("")));

        return !skipNodeContent;
    }

    @Override
    protected void postLeave(Visitable visitable) {
        separatorOnCurrentLevel().ifPresent(ref -> ref.set(", "));

        if (visitable instanceof ProvidesAffixes) {
            ((ProvidesAffixes) visitable).getSuffix().ifPresent(this::doWithSuffix);
        }

        if (visitable instanceof TypedSubtree) {
            enableSeparator(currentLevel + 1, false);
        }

        if (currentAliasedElements.peek() == visitable) {
            currentAliasedElements.pop();
        }

        if (visitable instanceof MapProjection) {
            this.skipAliasing = false;
        }

        if (visitable instanceof AliasedExpression) {
            AliasedExpression aliasedExpression = (AliasedExpression) visitable;
            visitableToAliased.add(aliasedExpression);
        }

        --currentLevel;
    }

    protected void doWithPrefix(String prefix) {
        this.builder.append(prefix);
    }

    protected void doWithSuffix(String suffix) {
        this.builder.append(suffix);
    }

    void enter(Match match) {
        if (match.isOptional()) {
            builder.append("OPTIONAL ");
        }
        builder.append("MATCH ");
    }

    void leave(Match match) {
        builder.append(" ");
    }

    void enter(Where where) {
        builder.append(" WHERE ");
    }

    void enter(RelationshipPatternCondition condition) {
        inRelationshipCondition = true;
    }

    void leave(RelationshipPatternCondition condition) {
        inRelationshipCondition = false;
    }

    void enter(Distinct distinct) {
        builder.append("DISTINCT ");
    }

    void enter(Return returning) {
        if (!returning.isRaw()) {
            builder.append("RETURN ");
        }
    }

    void enter(With with) {
        builder.append("WITH ");
    }

    void leave(With with) {
        builder.append(" ");
        clearPreviouslyVisitedNamed(with);
    }

    void leave(Statement.SingleQuery statement) {
        this.dequeOfVisitedNamed.peek().clear();
    }

    private void clearPreviouslyVisitedNamed(With with) {
        // We need to clear the named cache after defining a with.
        // Everything not taken into the next step has to go.
        Set<Named> retain = new HashSet<>();
        Set<Named> visitedNamed = dequeOfVisitedNamed.peek();
        with.accept(segment -> {
            if (segment instanceof SymbolicName) {
                visitedNamed.stream()
                        .filter(named -> named.getRequiredSymbolicName().equals(segment))
                        .forEach(retain::add);
            }
        });
        visitedNamed.retainAll(retain);
    }

    void enter(AliasedExpression aliased) {
        if (this.visitableToAliased.contains(aliased)) {
            builder.append(escapeIfNecessary(aliased.getAlias()));
        }
    }

    void leave(AliasedExpression aliased) {
        if (!(this.visitableToAliased.contains(aliased) || skipAliasing)) {
            builder.append(" AS ").append(escapeIfNecessary(aliased.getAlias()));
        }
    }

    void enter(NestedExpression nested) {
        builder.append("(");
    }

    void leave(NestedExpression nested) {
        builder.append(")");
    }

    void enter(Order order) {
        builder.append(" ORDER BY ");
    }

    void enter(Skip skip) {
        builder.append(" SKIP ");
    }

    void enter(Limit limit) {
        builder.append(" LIMIT ");
    }

    void enter(SortItem.Direction direction) {
        builder
                .append(" ")
                .append(direction.getSymbol());
    }

    void enter(PropertyLookup propertyLookup) {
        if (propertyLookup.isDynamicLookup()) {
            builder.append("[");
        } else {
            builder.append(".");
        }
    }

    void leave(PropertyLookup propertyLookup) {
        if (propertyLookup.isDynamicLookup()) {
            builder.append("]");
        }
    }

    void enter(FunctionInvocation functionInvocation) {
        builder
                .append(functionInvocation.getFunctionName())
                .append("(");
    }

    void leave(FunctionInvocation functionInvocation) {
        builder
                .append(")");
    }

    void enter(Operation operation) {
        if (operation.needsGrouping()) {
            builder.append("(");
        }
    }

    void enter(Operator operator) {
        Operator.Type type = operator.getType();
        if (type == Operator.Type.LABEL) {
            return;
        }
        boolean skipSpaces = SKIP_SPACES.contains(operator);
        if (type != Operator.Type.PREFIX && !skipSpaces) {
            builder.append(" ");
        }
        if (operator.equals(Operator.EQUALITY)) {
            builder.append("==");
        } else {
            builder.append(operator.getRepresentation());
        }
        if (type != Operator.Type.POSTFIX && !skipSpaces) {
            builder.append(" ");
        }
    }

    void leave(Operation operation) {
        if (operation.needsGrouping()) {
            builder.append(")");
        }
    }

    void enter(Literal<?> expression) {
        builder.append(expression.asString());
    }

    void enter(Node node) {
        builder.append("(");
        // This is only relevant for nodes in relationships.
        // Otherwise all the labels would be rendered again.
        Set<Named> visitedNamed = dequeOfVisitedNamed.peek();
        skipNodeContent = visitedNamed.contains(node);
        visitedNamed.add(node);

        if (skipNodeContent) {
            String symbolicName = node.getSymbolicName()
                    .map(SymbolicName::getValue).orElseGet(() -> resolve(node.getRequiredSymbolicName()));
            builder.append(symbolicName);
        }

        skipSymbolicName = inRelationshipCondition;
    }

    void leave(Node node) {
        builder.append(")");
        skipNodeContent = false;
        skipSymbolicName = false;
    }

    void enter(NodeLabel nodeLabel) {
        escapeName(nodeLabel.getValue()).ifPresent(label -> builder.append(":").append(label));
    }

    void enter(Properties properties) {
        builder.append(" ");
    }

    void enter(SymbolicName symbolicName) {
        if (skipSymbolicName) {
            return;
        }
        builder.append(resolve(symbolicName));
    }

    void enter(Relationship relationship) {
        Set<Named> visitedNamed = dequeOfVisitedNamed.peek();
        skipRelationshipContent = visitedNamed.contains(relationship);
        visitedNamed.add(relationship);

    }

    void enter(Relationship.Details details) {
        Relationship.Direction direction = details.getDirection();
        builder.append(direction.getSymbolLeft());
        if (details.hasContent()) {
            builder.append("[");
        }
        skipSymbolicName = inRelationshipCondition && !skipRelationshipContent;
    }

    void enter(RelationshipTypes types) {
        if (skipRelationshipContent) {
            return;
        }
        builder
                .append(types.getValues().stream()
                        .map(this::escapeName)
                        .map(Optional::get).collect(Collectors.joining("|", ":", "")));
    }

    void enter(RelationshipLength length) {
        if (skipRelationshipContent) {
            return;
        }

        Integer minimum = length.getMinimum();
        Integer maximum = length.getMaximum();

        if (length.isUnbounded()) {
            builder.append("*");
            return;
        }

        if (minimum == null && maximum == null) {
            return;
        }

        builder.append("*");
        if (minimum != null) {
            builder.append(minimum);
        }
        if (maximum != null) {
            builder.append("..");
            builder.append(maximum);
        }
    }

    void leave(Relationship.Details details) {
        Relationship.Direction direction = details.getDirection();
        if (details.hasContent()) {
            builder.append("]");
        }
        builder.append(direction.getSymbolRight());

        skipSymbolicName = false;
    }

    void leave(Relationship relationship) {
        skipRelationshipContent = false;
    }

    protected final void renderParameter(Parameter parameter) {
        if (statementContext == null) {
            throw new IllegalStateException("Parameter outside a statement context are not supported.");
        }
        builder.append("$").append(statementContext.getParameterName(parameter));
    }

    void enter(Parameter parameter) {
        Object value = parameter.getValue();
        if (value instanceof ConstantParameterHolder && !statementContext.isRenderConstantsAsParameters()) {
            builder.append(((ConstantParameterHolder) value).asString());
        } else {
            renderParameter(parameter);
        }
    }

    void enter(MapExpression map) {
        builder.append("{");
    }

    void enter(KeyValueMapEntry map) {
        builder.append(escapeIfNecessary(map.getKey())).append(": ");
    }

    void leave(MapExpression map) {
        builder.append("}");
    }

    void enter(ListExpression list) {
        builder.append("[");
    }

    void leave(ListExpression list) {
        builder.append("]");
    }

    void enter(Unwind unwind) {
        builder.append("UNWIND ");
    }

    void leave(Unwind unwind) {
        builder.append(" AS ")
                .append(unwind.getVariable())
                .append(" ");
    }

    void enter(UnionPart unionPart) {
        builder.append(" UNION ");
        if (unionPart.isAll()) {
            builder.append("ALL ");
        }
    }

    void enter(PatternComprehension patternComprehension) {
        builder.append("[");
    }

    void leave(PatternComprehension patternComprehension) {
        builder.append("]");
    }

    void enter(ListComprehension listComprehension) {
        builder.append("[");
    }

    void leave(ListComprehension listComprehension) {
        builder.append("]");
    }

    void enter(Case genericCase) {
        builder.append("CASE");
    }

    void enter(Case.SimpleCase simpleCase) {
        builder.append("CASE ");
    }

    void enter(CaseWhenThen caseWhenExpression) {
        builder.append(" WHEN ");
    }

    void leave(CaseWhenThen caseWhenExpression) {
        builder.append(" THEN ");
    }

    void enter(CaseElse caseElseExpression) {
        builder.append(" ELSE ");
    }

    void leave(Case caseExpression) {
        builder.append(" END");
    }

    void leave(Namespace namespace) {
        builder.append(".");
    }

    void leave(ProcedureName procedureName) {
        builder.append(procedureName.getValue());
    }

    void enter(YieldItems yieldItems) {
        builder.append(" YIELD ");
    }

    void enter(Enum<?> statement) {
        builder.append(statement.name().replaceAll("_", " ")).append(" ");
    }

    void enter(ExistentialSubquery subquery) {
        builder.append("EXISTS {");
    }

    void leave(ExistentialSubquery subquery) {
        // Trimming the inner match without having to do this in the match (looking up if inside subquery).
        builder.replace(builder.length() - 1, builder.length(), "}");
    }

    public String getRenderedContent() {
        return this.builder.toString();
    }

    /**
     * Escapes a symbolic name. Such a symbolic name is either used for a nodes label, the type of a relationship or a
     * variable.
     *
     * @param unescapedName The name to escape.
     * @return An empty optional when the unescaped name is {@literal null}, otherwise the correctly escaped name, safe to be used in statements.
     */
    protected Optional<String> escapeName(CharSequence unescapedName) {
        if (unescapedName == null) {
            return Optional.empty();
        }

        Matcher matcher = LABEL_AND_TYPE_QUOTATION.matcher(unescapedName);
        return Optional.of(String.format(Locale.ENGLISH, "`%s`", matcher.replaceAll("``")));
    }

    protected String escapeIfNecessary(String potentiallyNonIdentifier) {
        if (potentiallyNonIdentifier == null || Strings.isIdentifier(potentiallyNonIdentifier) || potentiallyNonIdentifier.trim().isEmpty()) {
            return potentiallyNonIdentifier;
        }

        Matcher matcher = LABEL_AND_TYPE_QUOTATION.matcher(potentiallyNonIdentifier);
        return String.format(Locale.ENGLISH, "`%s`", matcher.replaceAll("``"));
    }
}